package divestoclimb.checklist.storage;

import java.io.Reader;
import java.io.Writer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

import com.megginson.sax.DataWriter;

import android.database.Cursor;
import android.sax.*;
import android.util.Xml;

import divestoclimb.checklist.data.*;

/**
 * Utility class for XML input/output. Most methods are written in a modular
 * fashion to only deal with a single element and its children, without
 * knowledge of the entire structure of the document.
 * @author Ben Roberts (divestoclimb@gmail.com)
 */
public class XmlAdapter {

	public static final DateFormat ISO8601_STANDARD = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
	static {
		ISO8601_STANDARD.setTimeZone(TimeZone.getTimeZone("GMT"));
	}
	
	private DbAdapter mDbAdapter;
	private DataWriter mCurrentDocument;
	
	public XmlAdapter(DbAdapter d) {
		mDbAdapter = d;
		mCurrentDocument = null;
	}

	/**
	 * Initialize the DataWriter if we're starting a new document
	 * @param out The Writer to direct output to
	 * @return true if a new DataWriter was created (in which case you must
	 * call closeWriter when done) or false if a document is already being written.
	 * @throws SAXException
	 */
	private boolean openWriter(Writer out) throws SAXException {
		// If a document is not already in progress, create one
		if(mCurrentDocument == null) {
			mCurrentDocument = new DataWriter(out);
			mCurrentDocument.startDocument();
			return true;
		}
		return false;
	}
	
	private void closeWriter() {
		try {
			mCurrentDocument.endDocument();
		} catch (SAXException e) { }
		mCurrentDocument = null;
	}
	
	/**
	 * Outputs a check element
	 * @param c The Check object to output
	 * @param out The Writer to output the element to
	 * @throws SAXException
	 */
	private void writeCheck(Check c, Writer out) throws SAXException {
		AttributesImpl atts = new AttributesImpl();
		if(c.getContainerType() == DbAdapter.ITEM_CONTAINER_CHECKLIST) {
			String state;
			boolean show_date = false;
			switch(c.getState()) {
			case Check.STATE_CHECKED:
				state = "checked";
				show_date = true;
				break;
			case Check.STATE_SKIPPED:
				state = "skipped";
				show_date = true;
				break;
			default:
				state = "blank";
			}
			atts.addAttribute("", "state", "", "string", state);
			if(show_date) {
				atts.addAttribute("", "date", "", "string", ISO8601_STANDARD.format(c.getStateChangedDate()));
			}
		}
		mCurrentDocument.dataElement("", "check", "", atts, c.getText());
	}
	
	/**
	 * Outputs a note element
	 * @param n The Note object to output
	 * @param out The Writer to output the element to
	 * @throws SAXException
	 */
	private void writeNote(Note n, Writer out) throws SAXException {
		mCurrentDocument.dataElement("note", n.getText());
	}
	
	/**
	 * Outputs a series of checks and notes
	 * @param l The ItemList object to output as a list
	 * @param out The Writer to output the list to
	 * @throws SAXException
	 */
	private void writeItemList(ItemList l, Writer out) throws SAXException {
		Cursor cu = l.getCursor();
		cu.moveToFirst();
		while(! cu.isLast()) {
			// FIXME: This creates a new Item object per pass. Inefficient!
			Item i = DbAdapter.fetchItemFromCursor(cu);
			if(i instanceof Note) {
				writeNote((Note)i, out);
			} else {
				writeCheck((Check)i, out);
			}
			cu.moveToNext();
		}
	}

	/**
	 * Outputs a template and all its items
	 * @param t The Template object to output
	 * @param out The Writer to output the template to
	 * @return true if the operation succeeded, false otherwise
	 */
	public boolean writeTemplate(Template t, Writer out) {
		boolean success, opened = false;
		try {
			opened = openWriter(out);
			DataWriter dw = mCurrentDocument;
			AttributesImpl atts = new AttributesImpl();
			atts.addAttribute("", "name", "", "string", t.getName());
			atts.addAttribute("", "category", "", "string", t.getCategory().getName());
			if(t.getVersion() != null) {
				atts.addAttribute("", "templateVersion", "", "string", t.getVersion());
			}
			dw.startElement("", "checklist", "", atts);
			writeItemList(new ItemList(DbAdapter.ITEM_CONTAINER_TEMPLATE, t.getID(), mDbAdapter), out);
			dw.endElement("checklist");
			success = true;
		} catch(SAXException e) {
			success = false;
		}
		if(opened) {
			closeWriter();
		}
		return success;
	}
	
	/**
	 * Outputs a template and all its items
	 * @param template_id The ID of the template to output
	 * @param out The Writer to output the template to
	 * @return true if the operation succeeded, false otherwise
	 */
	public boolean writeTemplate(long template_id, Writer out) {
		return writeTemplate(mDbAdapter.fetchTemplate(template_id), out);
	}
	
	/**
	 * Outputs a checklist and all its items
	 * @param c The Checklist object to output
	 * @param out The Writer to output the checklist to
	 * @return true if the operation succeeded, false otherwise
	 */
	public boolean writeChecklist(Checklist c, Writer out) {
		boolean success, opened = false;
		try {
			opened = openWriter(out);
			DataWriter dw = mCurrentDocument;
			AttributesImpl atts = new AttributesImpl();
			atts.addAttribute("", "name", "", "string", c.getName());
			atts.addAttribute("", "category", "", "string", c.getCategory().getName());
			if(c.getTemplateName() != null) {
				atts.addAttribute("", "templateName", "", "string", c.getTemplateName());
				if(c.getVersion() != null) {
					atts.addAttribute("", "templateVersion", "", "string", c.getVersion());
				}
			}
			atts.addAttribute("", "started", "", "string", ISO8601_STANDARD.format(c.getStartDate()));
			if(c.getCompletedDate() != null) {
				atts.addAttribute("", "completed", "", "string", ISO8601_STANDARD.format(c.getCompletedDate()));
			}
			dw.startElement("", "checklist", "", atts);
			writeItemList(new ItemList(DbAdapter.ITEM_CONTAINER_CHECKLIST, c.getID(), mDbAdapter), out);
			dw.endElement("checklist");
			success = true;
		} catch(SAXException e) {
			success = false;
		}
		if(opened) {
			closeWriter();
		}
		return success;
	}
	
	/**
	 * Outputs a checklist and all its items
	 * @param checklist_id The ID of the checklist to output
	 * @param out The Writer to output the checklist to
	 * @return true if the operation succeeded, false otherwise
	 */
	public boolean writeChecklist(long checklist_id, Writer out) {
		return writeChecklist(mDbAdapter.fetchChecklist(checklist_id), out);
	}

	// Methods for reading XML checklists/templates
	
	// This tracks the order of the last item in a list. This is incremented
	// each time a new item is added to the database.
	private int mCurrentOrder;
	// This tracks the ID of the record corresponding to the parent element
	// for an item. The record is created when the parent element is read,
	// then used as the foreign key to build records for each child item.
	// This is reset to -1 after the container element is closed.
	private long mContainerId,
	// The methods for parsing a single container from an XML document
	// must return the ID of that container's new record. This field
	// is set to mContainerId when a container element ends, preserving
	// the last container's ID so it can be returned.
		mLastContainer;

	/**
	 * Android SAX listener for a check element
	 */
	private class CheckReader implements TextElementListener {
		
		private boolean mReadingChecklist, mInvalid;
		private Date mDate;
		private int mState;

		public CheckReader(boolean from_checklist) {
			mReadingChecklist = from_checklist;
		}

		public void start(Attributes attributes) {
			mInvalid = mContainerId == -1;
			if(mReadingChecklist) {
				try {
					String state = attributes.getValue("state");
					if(state.equals("checked")) {
						mState = Check.STATE_CHECKED;
						mDate = ISO8601_STANDARD.parse(attributes.getValue("date"));
					} else if(state.equals("skipped")) {
						mState = Check.STATE_SKIPPED;
						mDate = ISO8601_STANDARD.parse(attributes.getValue("date"));
					} else {
						mState = Check.STATE_BLANK;
						mDate = null;
					}
				} catch(Exception e) {
					mInvalid = true;
				}
			}
		}

		public void end(String body) {
			if(! mInvalid) {
				Check c = new Check(mReadingChecklist? DbAdapter.ITEM_CONTAINER_CHECKLIST:
						DbAdapter.ITEM_CONTAINER_TEMPLATE,
					mContainerId);
				if(mReadingChecklist) {
					c.setState(mState, mDate);
				}
				c.setText(body);
				c.setOrder(mCurrentOrder += ItemList.STD_ORDER_INCREMENT);
				c.save(mDbAdapter);
			}
		}

	}
	
	/**
	 * Android SAX listener for a note element
	 */
	private class NoteReader implements TextElementListener {

		private boolean mReadingChecklist, mInvalid;

		public NoteReader(boolean from_checklist) {
			mReadingChecklist = from_checklist;
		}

		public void start(Attributes attributes) {
			mInvalid = mContainerId == -1;
		}

		public void end(String body) {
			if(! mInvalid) {
				Note n = new Note(mReadingChecklist? DbAdapter.ITEM_CONTAINER_CHECKLIST:
						DbAdapter.ITEM_CONTAINER_TEMPLATE,
						mContainerId);
				n.setText(body);
				n.setOrder(mCurrentOrder+=8);
				n.save(mDbAdapter);
			}
		}
	}
	
	/**
	 * Android SAX listener for a template element
	 */
	private class TemplateReader implements ElementListener {
		private boolean mKeepCategory;
		public TemplateReader(boolean keepCategory) {
			mKeepCategory = keepCategory;
		}
		public void start(Attributes attributes) {
			mCurrentOrder = 0;
			try {
				final String name = attributes.getValue("name"),
					version = attributes.getValue("version");
				// Replace any template with the same name
				Template t = mDbAdapter.fetchTemplate(name);
				if(t != null) {
					mDbAdapter.deleteTemplate(t.getID());
				}
				t = new Template(name);
				t.setVersion(version);
				if(mKeepCategory) {
					final String category = attributes.getValue("category");
					Category cat = mDbAdapter.fetchCategory(category);
					if(cat == null) {
						cat = new Category(category);
						cat.save(mDbAdapter);
					}
					t.setCategory(cat);
				}
				t.save(mDbAdapter);
				mContainerId = t.getID();
			} catch(Exception e) {
				mContainerId = -1;
			}
		}

		public void end() {
			if(mContainerId != -1) {
				mLastContainer = mContainerId;
				mContainerId = -1;
			}
		}
	}
	
	/**
	 * Android SAX listener for a checklist element
	 */
	private class ChecklistReader implements ElementListener {
		private boolean mKeepCategory;
		public ChecklistReader(boolean keepCategory) {
			mKeepCategory = keepCategory;
		}
		public void start(Attributes attributes) {
			mCurrentOrder = 0;
			try {
				final String name = attributes.getValue("name"),
					templateName = attributes.getValue("templateName"),
					templateVersion = attributes.getValue("templateVersion"),
					strCompleted = attributes.getValue("completed");
				Date start_date = ISO8601_STANDARD.parse(attributes.getValue("started")),
					completed_date = strCompleted == null? null:
						ISO8601_STANDARD.parse(strCompleted);
				// Replace any checklist with the same name started at the same time
				Checklist c = mDbAdapter.fetchChecklist(name, start_date);
				if(c != null) {
					mDbAdapter.deleteChecklist(c.getID());
				}
				c = new Checklist(name, templateName, templateVersion, start_date, completed_date);
				if(mKeepCategory) {
					final String category = attributes.getValue("category");
					Category cat = mDbAdapter.fetchCategory(category);
					c.setCategory(cat);
				}
				c.save(mDbAdapter);
				mContainerId = c.getID();
			} catch(Exception e) {
				mContainerId = -1;
			}
		}
		
		public void end() {
			if(mContainerId != -1) {
				mLastContainer = mContainerId;
				mContainerId = -1;
			}
		}
	}
	
	/**
	 * Inernal function that parses a document with a single container element
	 * (template or checklist).
	 * @param in The Reader to use to access the document
	 * @param isChecklist true if the document should be parsed as a checklist,
	 * otherwise it is parsed as a template. The two formats are compatible with
	 * each other so a template could be parsed as a checklist and vice versa.
	 * @param keepCategory true if the category should be read from the document
	 * and auto-created if it does not exist. Otherwise the new container will
	 * be placed in the default category when it is created.
	 * @return The ID of the new container record in the database, or -1 if
	 * something went wrong.
	 */
	private long readContainer(Reader in, boolean isChecklist, boolean keepCategory) {
		RootElement root = new RootElement("checklist");
		Element check = root.getChild("check"),
			note = root.getChild("note");

		root.setElementListener(isChecklist? new ChecklistReader(keepCategory): new TemplateReader(keepCategory));
		
		check.setTextElementListener(new CheckReader(isChecklist));
		note.setTextElementListener(new NoteReader(isChecklist));

		mLastContainer = mContainerId = -1;
		
		try {
			Xml.parse(in, root.getContentHandler());
			return mLastContainer;
		} catch(Exception e) {
			return -1;
		}
	}
	
	/**
	 * Parse a XML document for a template and add it to the database.
	 * @param in The Reader to use to access the document
	 * @param keepCategory true if the category should be read from the document
	 * and auto-created if it does not exist. Otherwise the new container will
	 * be placed in the default category when it is created.
	 * @return The ID of the new template record in the database, or -1 if
	 * something went wrong.
	 */
	public long readTemplate(Reader in, boolean keepCategory) {
		return readContainer(in, false, keepCategory);
	}
	
	/**
	 * Parse a XML document for a checklist and add it to the database.
	 * @param in The Reader to use to access the document
	 * @param keepCategory true if the category should be read from the document
	 * and auto-created if it does not exist. Otherwise the new container will
	 * be placed in the default category when it is created.
	 * @return The ID of the new template record in the database, or -1 if
	 * something went wrong.
	 */
	public long readChecklist(Reader in, boolean keepCategory) {
		return readContainer(in, true, keepCategory);
	}
}